home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Nebula 2
/
Nebula Two.iso
/
SourceCode
/
GameKit
/
gamekit-1
/
ScorePlayer.m
< prev
next >
Wrap
Text File
|
1995-06-12
|
15KB
|
478 lines
// The ScorePlayer object handles reading scorefiles as
// well as starting and stopping playback. Playback is
// done in a separate thread. Errors are ignored.
// This code is a highly modified version of the code
// used in the ScorePlayer.app music kit example.
// I have added several methods and removed most of the
// error messages, since the errors are not useful in a game.
// Note: if you plan to use sounds simultaneously with the
// music, you CANNOT use the NeXT Sound object's -play
// method!!! You have to allocate and set up your own
// SoundOut and PlayStream objects and go through them.
// You can use the Sound object to store/convert the data,
// but not play it. See the SoundPlayer.[hm] files in this
// distribution to see how to accomplish this.
// This should be the default score to be loaded:
#define defaultFileName "Default.score"
#import <gamekit/gamekit.h>
#import <daymisckit/daymisckit.h>
#import <objc/NXBundle.h>
#import <musickit/musickit.h>
#import <string.h>
#import <libc.h>
#import <mach/cthreads.h>
#import <mach/mach.h>
#import <mach/mach_error.h>
#import <mach/message.h>
#import <objc/objc-runtime.h>
@implementation ScorePlayer
// Strings used in alert panel. Ought to be localized eventually.
#define OBJECTNAME "Load Score"
#define CANTLOAD "Unable to load music score file."
#define OK "OK"
static BOOL playScoreForm;
static id synthInstruments;
static id openPanel;
static char* fileName;
static id scoreObj,scorePerformer,theOrch;
static double samplingRate = 22050;
static double headroom = .1;
static BOOL userCancelFileRead = NO;
static double initialTempo = 60.0;
static double lastTempo = 60.0;
static double desiredTempo = 60.0;
static char *fileSuffixes[3] = {"score","playscore",NULL};
static id condClass = nil;
static id midis[2] = {0};
static int midiOffset;
static BOOL errorDuringPlayback = NO;
static BOOL firstPlay = YES;
#define PLAYING ([condClass performanceThread] != NO_CTHREAD)
#define SOUND_OUT_PAUSE_BUG 1 // Workaround for problem synching MIDI to DSP
static int handleObjcError(const char *className)
{ // ignore objc errors (like missing synthpatch classes)
return 0;
}
static void handleMKError(char *msg)
{ // ignore all errors
if (!PLAYING) { // if can't read file (ie. parse error), cancel read
userCancelFileRead = YES;
}
}
void cantLoad()
{
NXRunAlertPanel(OBJECTNAME, CANTLOAD, OK, NULL, NULL);
}
- _loadFile
{ // actually loads in the scorefile
id tuningSys;
id scoreInfo;
haveScore = NO; firstPlay = YES;
MKSetScorefileParseErrorAbort(10);
if ((!fileName) || (!strlen(fileName))) { /* Can this ever happen? */
return nil;
}
playScoreForm = (strstr(fileName,".playscore") != NULL);
[scoreObj free];
scoreObj = [Score new];
userCancelFileRead = NO;
tuningSys = [[TuningSystem alloc] init]; /* 12-tone equal tempered */
[tuningSys install];
[tuningSys free];
if (![scoreObj readScorefile:(char *)fileName] || userCancelFileRead) {
cantLoad();
scoreObj = [scoreObj free];
fileName[0] = '\0';
return nil;
}
samplingRate = 22050;
headroom = .1;
initialTempo = 60.0;
[[condClass defaultConductor] setTempo:initialTempo];
scoreInfo = [(Score *)scoreObj info];
if (scoreInfo) { /* Configure performance as specified in info. */
int midiOffsetPar;
midiOffset = 0;
midiOffsetPar = [Note parName:"midiOffset"];
if ([scoreInfo isParPresent:midiOffsetPar])
midiOffset = [scoreInfo parAsDouble:midiOffsetPar];
if ([scoreInfo isParPresent:MK_headroom])
headroom = [scoreInfo parAsDouble:MK_headroom];
if ([scoreInfo isParPresent:MK_samplingRate]) {
samplingRate = [scoreInfo parAsDouble:MK_samplingRate];
if (!((samplingRate == 44100.0) || (samplingRate == 22050.0))) {
samplingRate = 22050; // has to be one or the other!
}
}
if ([scoreInfo isParPresent:MK_tempo]) {
initialTempo = [scoreInfo parAsDouble:MK_tempo];
[[condClass defaultConductor] setTempo:initialTempo];
}
#if SOUND_OUT_PAUSE_BUG
if (samplingRate == 22050)
midiOffset += .36363636363636/8.0;
else midiOffset += .181818181818181/8.0;
#else
if (samplingRate == 22050)
midiOffset += .36363636363636;
else midiOffset += .181818181818181;
#endif
/* Note: there is a .1 second indeterminacy (in the 22khz case) due
to not knowing where we are in soundout buffering. Using more,
but smaller buffers would solve this. */
}
lastTempo = desiredTempo = initialTempo;
haveScore = YES;
return self;
}
static port_t endOfTimePort = PORT_NULL;
-endOfTime // called by the musickit thread
{ // when a performance completes
// int i;
msg_header_t msg = {0, /* msg_unused */
TRUE, /* msg_simple */
sizeof(msg_header_t),/* msg_size */
MSG_TYPE_NORMAL, /* msg_type */
0}; /* Fills in remaining fields */
[theOrch close]; /* This will block! */
// for (i=0; i<2; i++) {
// [midis[i] close];
// midis[i] = nil;
// }
[theOrch setSoundOut:YES];
msg.msg_local_port = PORT_NULL;
msg.msg_remote_port = endOfTimePort;
msg_send(&msg, SEND_TIMEOUT, 0);
return self;
}
void *endOfTimeProc(msg_header_t *msg,ScorePlayer *myself )
{
// Tell delegate that the score finished.
[myself scoreFinishedPlaying];
return myself;
}
static BOOL isMidiClassName(char *className)
{
return (className && ((strcmp(className,"midi") == 0) ||
(strcmp(className,"midi1") == 0) ||
(strcmp(className,"midi0") == 0)));
}
#if SOUND_OUT_PAUSE_BUG
static BOOL checkForMidi(Score *obj)
{
id subobjs;
int i,cnt;
id info;
subobjs = [obj parts];
if (!subobjs)
return NO;
cnt = [subobjs count];
for (i=0; i<cnt; i++) {
info = [(Part *)[subobjs objectAt:i] info];
if ([info isParPresent:MK_synthPatch] &&
(isMidiClassName([info parAsStringNoCopy:MK_synthPatch]))) {
[subobjs free];
return YES;
}
}
[subobjs free];
return NO;
}
#endif
- _playIt
{ // initiate playback in separate MK thread
int partCount,synthPatchCount,voices,i,whichMidi,midiChan;
char *className;
id partPerformers,synthPatchClass,partPerformer,partInfo,anIns,aPart;
// if (firstPlay) { /* Could keep these around, in repeat-play cases: */
// scorePerformer = [scorePerformer free];
// [synthInstruments freeObjects];
// synthInstruments = [synthInstruments free];
//}
theOrch = [Orchestra newOnDSP:0]; /* A noop if it exists */
[theOrch setHeadroom:headroom]; /* Must be reset for each play */
[theOrch setSamplingRate:samplingRate];
#if SOUND_OUT_PAUSE_BUG
if (checkForMidi(scoreObj))
[theOrch setFastResponse:YES];
else [theOrch setFastResponse:NO];
#endif
[theOrch setOutputCommandsFile:NULL];
[theOrch setOutputSoundfile:NULL];
[theOrch setSoundOut:YES];
if (![theOrch open]) { // can't get DSP, so abort
return nil;
}
//if (firstPlay) {
scorePerformer = [ScorePerformer new];
[scorePerformer setScore:scoreObj];
[(ScorePerformer *)scorePerformer activate];
partPerformers = [scorePerformer partPerformers];
partCount = [partPerformers count];
synthInstruments = [List new];
for (i = 0; i < partCount; i++) {
partPerformer = [partPerformers objectAt:i];
aPart = [partPerformer part];
partInfo = [(Part *)aPart info];
if ((!partInfo) || ![partInfo isParPresent:MK_synthPatch]) {
continue; // missing parm. Just ignore.
}
className = [partInfo parAsStringNoCopy:MK_synthPatch];
if (isMidiClassName(className)) {
midiChan = [partInfo parAsInt:MK_midiChan];
if ((midiChan == MAXINT) || (midiChan > 16))
midiChan = 1;
if (strcmp(className,"midi") == 0)
className = "midi1";
if (strcmp(className,"midi1") == 0)
whichMidi = 1;
else whichMidi = 0;
if (midis[whichMidi] == nil)
midis[whichMidi] = [Midi newOnDevice:className];
[[partPerformer noteSender] connect:
[midis[whichMidi] channelNoteReceiver:midiChan]];
} else {
synthPatchClass = (strlen(className) ?
[SynthPatch findSynthPatchClass:className] : nil);
if (!synthPatchClass) { /* Class not loaded in program? */
haveScore = NO;
cantLoad();
return nil;
/* We would prefer to do dynamic loading here. */
}
anIns = [SynthInstrument new];
[synthInstruments addObject:anIns];
[[partPerformer noteSender] connect:[anIns noteReceiver]];
[anIns setSynthPatchClass:synthPatchClass];
if (![partInfo isParPresent:MK_synthPatchCount])
continue;
voices = [partInfo parAsInt:MK_synthPatchCount];
synthPatchCount =
[anIns setSynthPatchCount:voices patchTemplate:
[synthPatchClass patchTemplateFor:partInfo]];
if (synthPatchCount < voices) { // ignore problem
}
}
}
// [partPerformers free];
//}
errorDuringPlayback = NO;
MKSetDeltaT(.75);
[Orchestra setTimed:YES];
[condClass afterPerformanceSel:@selector(endOfTime) to:self argCount:0];
for (i=0; i<2; i++)
[midis[i] openOutputOnly]; /* midis[i] is nil if not in use */
for (i=0; i<2; i++)
if (midiOffset > 0)
[midis[i] setLocalDeltaT:midiOffset];
else if (midiOffset < 0)
[theOrch setLocalDeltaT:-midiOffset];
for (i=0; i<2; i++)
[midis[i] run]; firstPlay = NO;
[theOrch run];
[condClass startPerformance];
return self;
}
extern void _MKSetConductorThreadMaxStress(int arg);
- init
{ // set up our object. I really ought to change to using a +new
// type of method since there should only ever be one ScorePlayer.
static int inited = 0;
int ec;
[super init];
if (inited++)
return self;
haveScore = NO;
condClass = [Conductor class];
[condClass setThreadPriority:1.0];
setuid(getuid()); /* Must be after setThreadPriority. */
[condClass useSeparateThread:YES];
/* These numbers could be endlessly tweaked */
MKSetLowDeltaTThreshold(.25);
MKSetHighDeltaTThreshold(.4);
_MKSetConductorThreadMaxStress(1000000); /* Don't do cthread_yields */
ec = port_allocate(task_self(), &endOfTimePort);
DPSAddPort(endOfTimePort,(DPSPortProc)endOfTimeProc,
sizeof(msg_header_t),(void *)self,30);
MKSetErrorProc(handleMKError);
objc_setClassHandler(handleObjcError);
return self;
}
- appDidInit:sender // forwarded by GameBrain -- just loads score
{
[self loadFile];
return self;
}
int setUpFile()
{ // use open panel to grab a score/playscore file.
int success;
char *shortFileName, *dir;
static BOOL firstTime = YES;
if (!openPanel)
openPanel = [OpenPanel new];
if ((firstTime) && !fileName)
success = [openPanel
runModalForDirectory:"/LocalLibrary/Music/Scores"
file:"Examp1.score"
types:(const char *const *)fileSuffixes];
else if (fileName) { // split into dir & name & run open panel
dir = NXCopyStringBuffer((const char *)fileName);
shortFileName = rindex(dir, '/') + 1;
shortFileName[0] = '\0'; // isolate directory
shortFileName = rindex(fileName, '/') + 1; // isolate filename
success = [openPanel
runModalForDirectory:dir
file:shortFileName
types:(const char *const *)fileSuffixes];
free(dir);
} else success = [openPanel
runModalForTypes:(const char *const *)fileSuffixes];
if (!success) return NO;
fileName = NXCopyStringBuffer((const char *)[openPanel filename]);
// save the choice.
NXWriteDefault ([NXApp appName], "ScoreName", fileName);
firstTime = NO;
return YES;
}
- _abort
{ // abort (stop) a performance
int i;
if (PLAYING) {
[condClass lockPerformance];
for (i=0; i<2; i++)
if (midis[i]) {
[midis[i] allNotesOff];
[midis[i] abort];
}
[theOrch abort];
[condClass finishPerformance];
[condClass unlockPerformance];
cthread_yield();
while (PLAYING) ; /* Make sure it's really done. */
}
return self;
}
// loading a file always stops playback, but restarts playing after
// the new file is loaded if music was playing before the load.
// this is the most useful behavior for a game, IMHO...
// to change this, make a subclass that does something like this
// for all three score file loading methods:
//
// -loadfile { [self stop:self]; return [super loadFile]; }
- loadFile
{ // load default file in.
BOOL wasPlaying = PLAYING; char *slashPos;
const char *tmpstr = NXGetDefaultValue ([NXApp appName], "ScoreName");
aborted = YES;
if (PLAYING) [self _abort];
if (fileName) free(fileName);
if (!tmpstr) { // if no default yet, use built in score
fileName = malloc(MAXPATHLEN);
strcpy(fileName, NXArgv[0]);
if (slashPos = strrchr(fileName, '/')) {
slashPos[1] = '\0';
} else {
strcpy(fileName, "./");
}
strcat(fileName, defaultFileName);
} else fileName = NXCopyStringBuffer(tmpstr);
[self _loadFile];
if (wasPlaying) [self play:self];
return self;
}
- readScoreFile:(const char *)pathName; // open scorefile (full pathname)
{ // get a scorefile. give full path!
BOOL wasPlaying = PLAYING;
aborted = YES;
if (PLAYING) [self _abort];
strcpy(fileName, pathName);
[self _loadFile];
if (wasPlaying) [self play:self];
return self;
}
- selectFile:sender
{ // get the scorefile to use
BOOL wasPlaying = PLAYING;
aborted = YES;
if (PLAYING) [self _abort];
if (!setUpFile(NULL)) {
return self;
}
[self _loadFile];
if (wasPlaying) [self play:self];
return self;
}
- play:sender
{ // initiate a performance
if ((!haveScore) || (!fileName) || (!strlen(fileName))) return nil;
if (PLAYING) return self;
aborted = NO;
[self _playIt];
return self;
}
- stop:sender
{ // stop a performance
aborted = YES;
if (PLAYING) [self _abort];
return self;
}
// set up a delegate
- delegate { return delegate; }
- setDelegate:newDelegate
{
id oldDelegate = delegate;
delegate = newDelegate;
return oldDelegate;
}
// delegate can implement this to be notified when a score
// finishes playing. If no delegate, default implementation
// is to start playing the score again.
- scoreFinishedPlaying
{
if (delegate) {
if ([delegate respondsTo:@selector(scoreFinishedPlaying)])
return [delegate scoreFinishedPlaying];
} else { // restart unless we were sent a -stop: message
if (!aborted) return [self play:self];
}
return self; // never actually get here but suppresses a warning
}
@end